using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.CodeDom;
using System.IO;

namespace Weever.Json.Serialization.v1
{
    internal class JsonSerializerBuilder
    {
        private Dictionary<Type, string> _writeMethods = new Dictionary<Type, string>();
        private TypeReflector _reflector = new TypeReflector();
        private const string Namespace = "Weever.Json.Serialization.v1";
        private const string CycleCheckerType = Namespace + ".CycleReferenceChecker";
        private const string CycleCheckerField = "_cycleChecker";
        private const string FactoryField = "_factory";
        private CodeDomProvider _domProvider = new Microsoft.CSharp.CSharpCodeProvider();

        public Type Build(Type type)
        {
            _writeMethods.Clear();
            FillWellKnownMethod();

            CodeDomProvider cdp = new Microsoft.CSharp.CSharpCodeProvider();
            string generatedTypeName = GetTypeName(type);
            CodeCompileUnit ccu = BuildCompileUnit(type, generatedTypeName);
            CompilerParameters param = new CompilerParameters();

            List<Type> allTypes = _reflector.GetAllTypes(type);

            param.ReferencedAssemblies.Add(typeof (IGeneratedJsonSerializer).Assembly.Location);
            param.ReferencedAssemblies.Add(typeof (IJsonSerializer).Assembly.Location);

            foreach (Type t in allTypes)
            {
                string assemblyPath = t.Assembly.Location;
                if (param.ReferencedAssemblies.Contains(assemblyPath)) 
                    continue;
                param.ReferencedAssemblies.Add(assemblyPath);
            }

#if DEBUG 
            param.TempFiles.KeepFiles = true;
            param.IncludeDebugInformation = true;
#endif
            CompilerResults cr = cdp.CompileAssemblyFromDom(param, ccu);
            
            if (cr.Errors.Count > 0)
            {
                foreach (object error in cr.Errors)
                    System.Diagnostics.Debug.WriteLine(error);
                throw new Exception("Code generation error");
            }
            return cr.CompiledAssembly.GetType(generatedTypeName);
        }

        private CodeCompileUnit BuildCompileUnit(Type type, string name)
        {
            CodeCompileUnit ccu = new CodeCompileUnit();

            ccu.Namespaces.Add(BuildNamespace(type, name));

            return ccu;
        }

        private CodeNamespace BuildNamespace(Type type, string name)
        {
            int lastDotIndex = name.LastIndexOf(".");

            CodeNamespace cn = new CodeNamespace(name.Substring(0, lastDotIndex));
            cn.Types.Add(BuildJsonSerializer(type, name.Substring(lastDotIndex + 1)));
            return cn;
        }

        private CodeTypeDeclaration BuildJsonSerializer(Type type, string name)
        {
            CodeTypeDeclaration ctd = new CodeTypeDeclaration(name);
            ctd.Name = name;
            ctd.IsClass = true;
            ctd.BaseTypes.Add(typeof (IGeneratedJsonSerializer));
            ctd.Attributes = MemberAttributes.Public;
            ctd.Members.Add(BuildConstructor());
            ctd.Members.Add(new CodeMemberField(typeof(CycleReferenceChecker), CycleCheckerField));
            ctd.Members.Add(new CodeMemberField(typeof(IJsonSerializerFactory), FactoryField));
            ctd.Members.Add(BuildTextWriterSerializeMethod(ctd, type));
            ctd.Members.Add(BuildJsonWriterSerializeMethod(ctd, type));
            ctd.Members.Add(BuildDeserializeMethod(ctd, type));
            ctd.Members.Add(BuildDeserializeWidthFactoryMethod(ctd, type));
            ctd.Members.Add(BuildCloneMethod(ctd));
            return ctd;
        }

        private CodeConstructor BuildConstructor()
        {
            CodeConstructor cc = new CodeConstructor();
            cc.Attributes = MemberAttributes.Public;
            cc.Parameters.Add(new CodeParameterDeclarationExpression(typeof (IJsonSerializerFactory), "factory"));
            cc.Statements.Add(BuildStatement("{0} = new {1}();", CycleCheckerField, CycleCheckerType));
            cc.Statements.Add(BuildStatement("{0} = factory;", FactoryField));
            return cc;
        }

        private CodeMemberMethod BuildTextWriterSerializeMethod(CodeTypeDeclaration ctd, Type type)
        {
            CodeMemberMethod cmm = new CodeMemberMethod();
            cmm.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            cmm.Name = "Serialize";
            cmm.Parameters.Add(new CodeParameterDeclarationExpression(typeof (TextWriter), "textWriter"));
            cmm.Parameters.Add(new CodeParameterDeclarationExpression(typeof (object), "obj"));
            cmm.Statements.Add(BuildArgumentNotNullCheck("textWriter"));
            cmm.Statements.Add(BuildVarDeclaration("val", "obj", type));
            cmm.Statements.Add(
                BuildStatement("{0} writer = {1}.CreateWriter(textWriter);", typeof (IJsonWriter).ToString(), FactoryField));
            string methodCallTemplate;
            if (_writeMethods.TryGetValue(type, out methodCallTemplate))
                cmm.Statements.Add(BuildStatement(methodCallTemplate, "val"));
            else
                cmm.Statements.AddRange(BuildMembersSerialization(ctd, type));
            return cmm;
        }

        private CodeMemberMethod BuildJsonWriterSerializeMethod(CodeTypeDeclaration ctd, Type type)
        {
            CodeMemberMethod cmm = new CodeMemberMethod();
            cmm.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            cmm.Name = "Serialize";
            cmm.Parameters.Add(new CodeParameterDeclarationExpression(typeof(IJsonWriter), "writer"));
            cmm.Parameters.Add(new CodeParameterDeclarationExpression(typeof(object), "obj"));
            cmm.Statements.Add(BuildArgumentNotNullCheck("writer"));
            cmm.Statements.Add(BuildVarDeclaration("val", "obj", type));
            string methodCallTemplate;
            if (_writeMethods.TryGetValue(type, out methodCallTemplate))
                cmm.Statements.Add(BuildStatement(methodCallTemplate, "val"));
            else
                cmm.Statements.AddRange(BuildMembersSerialization(ctd, type));
            return cmm;
        }

        private void BuildTypedSerializeMethod(CodeTypeDeclaration ctd, string methodName, Type type)
        {
            CodeMemberMethod cmm = new CodeMemberMethod();
            cmm.Attributes = MemberAttributes.Private;
            cmm.Name = methodName;
            cmm.Parameters.Add(new CodeParameterDeclarationExpression(typeof(IJsonWriter), "writer"));
            cmm.Parameters.Add(new CodeParameterDeclarationExpression(type, "val"));
            cmm.Statements.AddRange(BuildMembersSerialization(ctd, type));
            ctd.Members.Add(cmm);
        }

        private CodeMemberMethod BuildDeserializeMethod(CodeTypeDeclaration ctd, Type type)
        {
            CodeMemberMethod cmm = new CodeMemberMethod();
            cmm.Attributes = MemberAttributes.Public;
            cmm.Name = "Deserialize";
            cmm.Parameters.Add(new CodeParameterDeclarationExpression(typeof(TextReader), "reader"));
            cmm.Statements.Add(BuildArgumentNotNullCheck("reader"));
            cmm.Statements.Add(BuildStatement("return null;"));
            cmm.ReturnType = new CodeTypeReference(typeof(object));
            return cmm;
        }

        private CodeTypeMember BuildDeserializeWidthFactoryMethod(CodeTypeDeclaration ctd, Type type)
        {
            CodeMemberMethod cmm = new CodeMemberMethod();
            cmm.Attributes = MemberAttributes.Public;
            cmm.Name = "Deserialize";
            cmm.Parameters.Add(new CodeParameterDeclarationExpression(typeof(TextReader), "reader"));
            cmm.Parameters.Add(new CodeParameterDeclarationExpression(typeof(FactoryMethod), "factoryMethod"));
            cmm.Statements.Add(BuildArgumentNotNullCheck("reader"));
            cmm.Statements.Add(BuildArgumentNotNullCheck("factoryMethod"));
            cmm.Statements.Add(new CodeSnippetStatement("return factoryMethod();"));
            cmm.ReturnType = new CodeTypeReference(typeof(object));
            return cmm;
        }

        private CodeMemberMethod BuildCloneMethod(CodeTypeDeclaration ctd)
        {
            CodeMemberMethod cmm = new CodeMemberMethod();
            cmm.Attributes = MemberAttributes.Public;
            cmm.Name = "Clone";
            cmm.ReturnType = new CodeTypeReference(typeof (object));
            cmm.Statements.Add(BuildStatement("{0} result = ({0})MemberwiseClone();", ctd.Name));
            cmm.Statements.Add(BuildStatement("result.{0} = new {1}();", CycleCheckerField, CycleCheckerType));
            cmm.Statements.Add(BuildStatement("return result;"));
            return cmm;
        }

        private CodeStatementCollection BuildMembersSerialization(CodeTypeDeclaration ctd, Type type)
        {
            Type elementType;
            Type keyType;
            if (TypeReflector.TypeIsDictionary(type, out keyType, out elementType))
                return BuildDictionaryMembersSerialization(ctd, type, keyType, elementType);
            if (TypeReflector.TypeIsCollection(type, out elementType))
                return BuildCollectionMembersSerialization(ctd, type, elementType);

            CodeStatementCollection csc = new CodeStatementCollection();
            if (!type.IsValueType)
            {
                csc.Add(BuildStatement("if (val == null) {writer.WriteNull(); return;}"));
                csc.Add(BuildStatement("if (!{0}.Push(val)){{writer.WriteNull(); return;}}", CycleCheckerField));
            }

            ReflectedMemberInfo[] infos = _reflector.GetSerializableMembers(type);

            csc.Add(BuildStatement("writer.WriteBeginObject();"));
            for (int i = 0; i < infos.Length - 1; i++)
            {
                ReflectedMemberInfo info = infos[i];
                BuildMemberSerialization(ctd, csc, info);
                csc.Add(BuildStatement("writer.WriteEndProperty();"));
            }
            if (infos.Length > 0)
                BuildMemberSerialization(ctd, csc, infos[infos.Length - 1]);
            csc.Add(BuildStatement("writer.WriteEndObject();"));
            if (!type.IsValueType)
                csc.Add(BuildStatement("{0}.Pop(val);", CycleCheckerField));
            return csc;
        }

        private CodeStatementCollection BuildDictionaryMembersSerialization(CodeTypeDeclaration ctd, Type type, Type keyType, Type elementType)
        {
            CodeStatementCollection csc = new CodeStatementCollection();
            if (!type.IsValueType)
            {
                csc.Add(BuildStatement("if (val == null) {writer.WriteNull(); return;}"));
                csc.Add(BuildStatement("if (!{0}.Push(val)){{writer.WriteNull(); return;}}", CycleCheckerField));
            }

            string methodCallTemplate;

            if (!_writeMethods.TryGetValue(elementType, out methodCallTemplate))
            {
                string methodName = GetMethodName(elementType);
                methodCallTemplate = "this." + methodName + "(writer, {0});";
                _writeMethods.Add(elementType, methodCallTemplate);
                BuildTypedSerializeMethod(ctd, methodName, elementType);
            }

            csc.Add(BuildStatement("writer.WriteBeginObject();"));
            
            string keyValueTypeName =
                _domProvider.GetTypeOutput(new CodeTypeReference("System.Collections.Generic.KeyValuePair`2", 
                    new CodeTypeReference(keyType), 
                    new CodeTypeReference(elementType)));

            csc.Add(BuildStatement( "int count = 0;"
                                  + "foreach ({0} keyVal in val)"
                                  + "{{"
                                  + "      if (count++ != 0)"
                                  + "            writer.WriteEndProperty();"
                                  + "      writer.WriteName(keyVal.Key.ToString());"
                                  + "      {1}"
                                  + "}}"
                                  , keyValueTypeName
                                  , string.Format(methodCallTemplate, "keyVal.Value")));

            csc.Add(BuildStatement("writer.WriteEndObject();"));

            if (!type.IsValueType)
                csc.Add(BuildStatement("{0}.Pop(val);", CycleCheckerField));
            return csc;
        }

        private CodeStatementCollection BuildCollectionMembersSerialization(CodeTypeDeclaration ctd, Type type, Type elementType)
        {
            CodeStatementCollection csc = new CodeStatementCollection();
            if (!type.IsValueType)
            {
                csc.Add(BuildStatement("if (val == null) {writer.WriteNull(); return;}"));
                csc.Add(BuildStatement("if (!{0}.Push(val)){{writer.WriteNull(); return;}}", CycleCheckerField));
            }
            
            string methodCallTemplate;

            if (!_writeMethods.TryGetValue(elementType, out methodCallTemplate))
            {
                string methodName = GetMethodName(elementType);
                methodCallTemplate = "this." + methodName + "(writer, {0});";
                _writeMethods.Add(elementType, methodCallTemplate);
                BuildTypedSerializeMethod(ctd, methodName, elementType);
            }

            string quanitfier = type.IsArray ? "Length" : "Count";

            csc.Add(BuildStatement("writer.WriteBeginArray();"));

            
            csc.Add(BuildStatement( "int count = 0;"
                                  + "for (int i = 0; i < val.{1}; i++) {{ "
                                  + "    if (count++ != 0)"
                                  + "        writer.WriteEndProperty();"
                                  + "    {0} "
                                  + " }}",
                                  string.Format(methodCallTemplate, "val[i]"), quanitfier));
            
            csc.Add(BuildStatement("writer.WriteEndArray();"));

            if (!type.IsValueType)
                csc.Add(BuildStatement("{0}.Pop(val);", CycleCheckerField));
            return csc;
        }

        private void BuildMemberSerialization(CodeTypeDeclaration ctd, CodeStatementCollection csc, ReflectedMemberInfo info)
        {
            csc.Add(BuildStatement(@"writer.WriteName(""{0}"");", info.JsonName));

            string methodCallTemplate;

            if (_writeMethods.TryGetValue(info.MemberType, out methodCallTemplate))
            {
                csc.Add(BuildStatement(methodCallTemplate, "val." + info.MemberInfo.Name));
            } 
            else
            {
                string methodName = GetMethodName(info.MemberType);
                methodCallTemplate = "this." + methodName + "(writer, {0});";
                _writeMethods.Add(info.MemberType, methodCallTemplate);
                BuildTypedSerializeMethod(ctd, methodName, info.MemberType);
                csc.Add(BuildStatement(methodCallTemplate, "val." + info.MemberInfo.Name));
            }
        }

        private string GetTypeName(Type type)
        {
            return type.Namespace + "." + type.Name.Replace("[]", "_array").Replace("`", "_T") + "_JsonSerializer";
        }

        private void FillWellKnownMethod()
        {
            foreach (Type type in _reflector.WellKnownTypes)
            {
                _writeMethods.Add(type, string.Format("writer.Write({{0}});"));
            }
        }

        #region Utils

        private string GetMethodName(Type type)
        {
            return "Serialize" + type.Name.Replace("[]", "_array").Replace("`", "_T").Replace("*", "_st") + "_" + _writeMethods.Count;
        }

        private CodeStatement BuildArgumentNotNullCheck(string argName)
        {
            return BuildStatement("if ({0} == null) throw new System.ArgumentNullException(\"{0}\");", argName);
        }

        private CodeSnippetStatement BuildStatement(string statement)
        {
            return new CodeSnippetStatement(statement);
        }

        private CodeSnippetStatement BuildStatement(string format, params string [] args )
        {
            return BuildStatement(string.Format(format, args));
        }

        private CodeVariableDeclarationStatement BuildVarDeclaration(string varName, string initObj, Type type)
        {
            CodeVariableDeclarationStatement stm =
                new CodeVariableDeclarationStatement(type, varName,
                                                     new CodeCastExpression(type,
                                                                            new CodeSnippetExpression(initObj)));
            return stm;
        }

        #endregion
    }
}
